﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;

namespace CSW.Framework.Common.Utils
{
    /// <summary>
    /// PathUtils.
    /// </summary>
    public static class PathUtils
    {
        private static readonly string m_ApplicationDirectory;
        private static readonly string m_LocalDataDirectory;
        private static readonly List<char> m_InvalidFileNameChars;
        private static readonly List<char> m_InvalidPathChars;

        static PathUtils()
        {
            // exit if design mode
            if (EntryAssembly.Assembly == null)
                return;

            // Get invalid file name and path chars.
            m_InvalidFileNameChars = new List<char>(Path.GetInvalidFileNameChars());
            m_InvalidPathChars = new List<char>(Path.GetInvalidPathChars());
            m_InvalidPathChars.AddRange(new[] { '?', '*' });

            // Get the directory where the entry assembly is located.
            m_ApplicationDirectory = Path.GetDirectoryName(EntryAssembly.Location);

            // Create the subfolder string
            string companyName = RemoveInvalidFileNameChars(EntryAssembly.CompanyName);
            string productName = RemoveInvalidFileNameChars(EntryAssembly.ProductName);

            string subFolder;
            if (companyName != null && companyName.Trim() != string.Empty)
                subFolder = Path.Combine(companyName, productName);
            else
                subFolder = productName;

            // Build the full LocalDataDirectory path and create it if it doesn't exist.
            string localUserDataDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            m_LocalDataDirectory = Path.Combine(localUserDataDirectory, subFolder);

            if (!Directory.Exists(m_LocalDataDirectory))
                Directory.CreateDirectory(m_LocalDataDirectory);
        }

        /// <summary>
        /// Removes invalid file name characters from a string.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <returns>The name of the file with invalid characters removed.</returns>
        public static string RemoveInvalidFileNameChars(string fileName)
        {
            return ReplaceInvalidFileNameChars(fileName, null);
        }

        /// <summary>
        /// Removes invalid path characters from a string.
        /// </summary>
        /// <param name="path">The path name.</param>
        /// <returns>The path name with invalid characters removed.</returns>
        public static string RemoveInvalidPathChars(string path)
        {
            return ReplaceInvalidPathChars(path, null);
        }

        /// <summary>
        /// Replaces invalid file name characters in a string.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <param name="newValue">A <see cref="String"/> to replace all occurrences of invalid characters.</param>
        /// <returns>The name of the file with invalid characters replaced.</returns>
        public static string ReplaceInvalidFileNameChars(string fileName, string newValue)
        {
            return FixEndsWithPeriod(ReplaceChars(fileName, newValue, m_InvalidFileNameChars));
        }

        /// <summary>
        /// Replaces invalid path characters in a string.
        /// </summary>
        /// <param name="path">The path name.</param>
        /// <param name="newValue">A <see cref="String"/> to replace all occurrences of invalid characters.</param>
        /// <returns>The path name with invalid characters replaced.</returns>
        public static string ReplaceInvalidPathChars(string path, string newValue)
        {
            return FixEndsWithPeriod(ReplaceChars(path, newValue, m_InvalidPathChars));
        }

        /// <summary>
        /// Replaces invalid file name characters in a string.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <param name="newValue">A <see cref="Char"/> to replace all occurrences of invalid characters.</param>
        /// <returns>The name of the file with invalid characters replaced.</returns>
        public static string ReplaceInvalidFileNameChars(string fileName, char newValue)
        {
            return FixEndsWithPeriod(ReplaceChars(fileName, newValue, m_InvalidFileNameChars));
        }

        /// <summary>
        /// Replaces invalid path characters in a string.
        /// </summary>
        /// <param name="path">The path name.</param>
        /// <param name="newValue">A <see cref="Char"/> to replace all occurrences of invalid characters.</param>
        /// <returns>The path name with invalid characters replaced.</returns>
        public static string ReplaceInvalidPathChars(string path, char newValue)
        {
            return FixEndsWithPeriod(ReplaceChars(path, newValue, m_InvalidPathChars));
        }

        private static string FixEndsWithPeriod(string input)
        {
            if (input == null)
                return input;

            if (input.Contains(@".\") || input.EndsWith("."))
            {
                int oldLength;
                do
                {
                    oldLength = input.Length;
                    input = input.Replace(@".\", @"\");
                    if (input.EndsWith("."))
                        input = input.Substring(0, input.Length - 1);
                } while (input.Length != oldLength);
            }

            return input;
        }

        private static string ReplaceChars(string input, string newValue, IEnumerable<char> invalidChars)
        {
            Guard.ArgumentNotNull(invalidChars, "invalidChars");

            if (input == null)
                return null;

            foreach (char invalidChar in invalidChars)
                input = input.Replace(invalidChar.ToString(), newValue);

            return input;
        }

        private static string ReplaceChars(string input, char newValue, IEnumerable<char> invalidChars)
        {
            Guard.ArgumentNotNull(invalidChars, "invalidChars");

            if (input == null)
                return null;

            foreach (char invalidChar in invalidChars)
                input = input.Replace(invalidChar, newValue);

            return input;
        }

        /// <summary>
        /// Gets the relative path of a full path related to a base path.
        /// </summary>
        /// <param name="basePath">The base path.</param>
        /// <param name="fullPath">The full path.</param>
        /// <returns>The relative path.</returns>
        public static string GetRelativePath(string basePath, string fullPath)
        {
            Guard.ArgumentNotNullOrEmptyString(basePath, "basePath");
            Guard.ArgumentNotNullOrEmptyString(fullPath, "fullPath");

            if (fullPath.StartsWith(basePath, true, CultureInfo.CurrentCulture))
            {
                fullPath = fullPath.Remove(0, basePath.Length);
                if (fullPath.StartsWith(@"\"))
                    fullPath = fullPath.Remove(0, 1);

                return fullPath;
            }
            else
            {
                throw new ArgumentException(string.Format("basePath '{0}' is not a base path of fullPath '{1}'", basePath, fullPath), "fullPath");
            }
        }

        /// <summary>
        /// Gets the directory where the entry assembly is located.
        /// </summary>
        /// <value>The directory where the entry assembly is located.</value>
        public static string ApplicationDirectory
        {
            get { return m_ApplicationDirectory; }
        }

        /// <summary>
        /// Gets the application specific data directory for the current local user.
        /// The base path is <see cref="Environment.SpecialFolder.LocalApplicationData"/>, 
        /// followed by the entry assembly's <see cref="AssemblyCompanyAttribute"/>
        /// (if set), followed by the entry assembly's <see cref="AssemblyProductAttribute"/>.
        /// If <see cref="AssemblyProductAttribute"/> is not set, then the entry assembly's
        /// <see cref="AssemblyName.Name"/> is used in its place.
        /// </summary>
        /// <value>The application specific data directory for the current local user.</value>
        public static string LocalDataDirectory
        {
            get { return m_LocalDataDirectory; }
        }

        /// <summary>
        /// Gets unique file name which does not exist based on the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="isUnique">The method which returns <c>true</c> if the file name is unique; otherwise, <c>false</c>.</param>
        /// <returns>Unique file name which does not exist.</returns>
        public static string GetUniqueFileName(string path, Func<string, bool> isUnique)
        {
            Guard.ArgumentNotNullOrEmptyString(path, "path");
            Guard.ArgumentNotNull(isUnique, "isUnique");

            if (isUnique(path))
                return path;

            string basePath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path));
            string ext = Path.GetExtension(path);

            for (int i = 1; i == 1 || !isUnique(path); i++)
            {
                path = string.Format("{0} ({1}){2}", basePath, i, ext);
            }

            return path;
        }

        /// <summary>
        /// Gets a unique file name which does not exist based on the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>Unique file name which does not exist.</returns>
        public static string GetUniqueFileName(string path)
        {
            Guard.ArgumentNotNullOrEmptyString(path, "path");

            if (!File.Exists(path))
                return path;

            string basePath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path));
            string ext = Path.GetExtension(path);

            for (int i = 1; i == 1 || File.Exists(path); i++)
            {
                path = string.Format("{0} ({1}){2}", basePath, i, ext);
            }

            return path;
        }

        /// <summary>
        /// Returns a unique file name in the temporary path with the specified extension.
        /// </summary>
        /// <param name="extension">The extension.</param>
        /// <returns>A unique file name in the temporary path with the specified extension.</returns>
        public static string GetTempFileName(string extension)
        {
            Guard.ArgumentNotNullOrEmptyString(extension, "extension");

            if (extension[0] != '.')
                extension = "." + extension;

            string tempPath = Path.GetTempPath();
            string guid = Guid.NewGuid().ToString();

            string fileName = Path.Combine(tempPath, guid) + extension;
            return GetUniqueFileName(fileName);
        }

        /// <summary>
        /// Touches the specified file causing its LastWriteTime to update.
        /// </summary>
        /// <param name="fileName">The full path of the file.</param>
        public static void Touch(string fileName)
        {
            FileInfo fi = new FileInfo(fileName);
            fi.LastWriteTime = DateTime.Now;
        }

        /// <summary>
        /// Gets a unique directory name which does not exist based on the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>Unique directory name which does not exist.</returns>
        public static string GetUniqueDirectoryName(string path)
        {
            Guard.ArgumentNotNullOrEmptyString(path, "path");

            if (!Directory.Exists(path))
                return path;

            string basePath = path;

            for (int i = 1; i == 1 || Directory.Exists(path); i++)
            {
                path = string.Format("{0} ({1})", basePath, i);
            }

            return path;
        }

        /// <summary>
        /// Deletes a directory if it exists. If an exception is throw, the directory path is included in the exception message.
        /// </summary>
        /// <param name="path">The directory.</param>
        public static void DeleteDirectoryIfExists(string path)
        {
            if (Directory.Exists(path))
            {
                try
                {
                    Directory.Delete(path);
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Directory \"{0}\"", path), ex);
                }
            }
        }
    }
}
